<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>拼豆模拟器 — Pixel Bead Simulator</title>
    <style>
        :root{
            --ui-bg:#f6f7fb;
            --panel-bg:#ffffff;
            --accent:#4f46e5;
            --muted:#6b7280;
        }
        *{box-sizing:border-box}
        body{
            font-family:Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
            margin:0;
            padding:18px;
            background:linear-gradient(180deg,var(--ui-bg),#ffffff);
            color:#111827;
        }
        .app{
            display:grid;
            grid-template-columns:320px 1fr;
            gap:18px;
            align-items:start;
            max-width:1200px;
            margin:0 auto;
        }
        .panel{
            background:var(--panel-bg);
            padding:14px;
            border-radius:12px;
            box-shadow:0 6px 18px rgba(15,23,42,0.06);
        }
        h1{font-size:18px;margin:0 0 8px}
        .controls{display:flex;flex-direction:column;gap:10px}
        label{font-size:13px;color:var(--muted)}
        .row{display:flex;gap:10px;align-items:center}
        .color-sample{width:34px;height:34px;border-radius:6px;border:1px solid #ddd}
        select,input[type=number]{padding:8px;border-radius:8px;border:1px solid #e5e7eb;background:#fff}
        button{padding:8px 10px;border-radius:8px;border:0;background:var(--accent);color:white;cursor:pointer}
        .btn-ghost{background:transparent;color:var(--muted);border:1px solid #e6e7eb}
        .small{padding:6px 8px;font-size:13px}
        .canvas-wrap{display:flex;flex-direction:column;gap:12px;align-items:center}
        #board{
            background:repeating-conic-gradient(#0000 0 25%, #0003 0 50%);
            border-radius:12px;
            box-shadow:0 8px 30px rgba(15,23,42,0.08);
        }
        .hint{font-size:13px;color:var(--muted)}
        footer{margin-top:14px;font-size:12px;color:var(--muted)}
        @media(max-width:980px){
            .app{grid-template-columns:1fr}
        }
    </style>
</head>
<body>
<div class="app">
    <div class="panel">
        <h1>拼豆模拟器（Pixel Bead Simulator）</h1>
        <div class="controls">
            <div>
                <label>颜色选择</label>
                <div class="row" style="margin-top:6px">
                    <input id="color" type="color" value="#ff3b30" style="height:36px;width:56px;border-radius:8px;border:1px solid #e6e7eb;padding:0" />
                    <div class="color-sample" id="colorPreview" title="当前颜色"></div>
                    <button id="eraser" class="btn-ghost small">橡皮（按住 Shift 切换）</button>
                </div>
            </div>

            <div>
                <label>网格大小</label>
                <div class="row" style="margin-top:6px">
                    <select id="gridSize">
                        <option value="16">16 × 16</option>
                        <option value="24">24 × 24</option>
                        <option value="32" selected>32 × 32</option>
                        <option value="48">48 × 48</option>
                        <option value="64">64 × 64</option>
                    </select>
                    <label style="display:flex;align-items:center;font-size:13px;color:var(--muted)"><input id="showGridLines" type="checkbox" style="margin-right:8px" checked /> 显示网格线</label>
                </div>
            </div>

            <div>
                <label>操作</label>
                <div class="row" style="margin-top:6px">
                    <button id="clear" class="small btn-ghost">清空</button>
                    <button id="fillAll" class="small btn-ghost">填充为当前颜色</button>
                    <button id="export" class="small">导出 PNG</button>
                    <button id="downloadData" class="small btn-ghost">导出 JSON</button>
                </div>
                <div class="hint" style="margin-top:8px">左键绘制，按住并拖动连续绘制；按住 <strong>Shift</strong> 切换为橡皮擦；右键清除单元格。</div>
            </div>

            <div>
                <label>缩放（显示）</label>
                <div class="row" style="margin-top:6px">
                    <input id="displaySize" type="number" min="200" max="1600" step="20" value="640" /> px
                </div>
            </div>

            <footer>打开此 HTML 文件即可在本地使用。喜欢的话我可以加撤销/重做、调色板、导入图像自动像素化等功能。</footer>
        </div>
    </div>

    <div class="panel canvas-wrap">
        <div style="display:flex;gap:12px;align-items:center;width:100%;justify-content:space-between">
            <h1 style="font-size:16px;margin:0">画布预览</h1>
            <div style="text-align:right;color:var(--muted)">当前：<span id="currentInfo">32 × 32</span></div>
        </div>

        <canvas id="board" width="640" height="640" style="touch-action:none"></canvas>
        <div class="hint">导出后得到的图片大小 = (网格大小 × 每格像素)。导出时每格像素默认为 16px。</div>
    </div>
</div>

<script>
    // ——— Core state ———
    const colorInput = document.getElementById('color');
    const colorPreview = document.getElementById('colorPreview');
    const eraserBtn = document.getElementById('eraser');
    const gridSelect = document.getElementById('gridSize');
    const showGridLines = document.getElementById('showGridLines');
    const clearBtn = document.getElementById('clear');
    const fillAllBtn = document.getElementById('fillAll');
    const exportBtn = document.getElementById('export');
    const downloadDataBtn = document.getElementById('downloadData');
    const displaySize = document.getElementById('displaySize');
    const canvas = document.getElementById('board');
    const ctx = canvas.getContext('2d', {alpha:false});
    const currentInfo = document.getElementById('currentInfo');

    let gridN = parseInt(gridSelect.value,10); // N x N
    let cellPixel = 20; // on-screen cell pixel size (computed from displaySize)
    let cells = createCells(gridN);
    let drawing = false;
    let isEraser = false;
    let currentColor = colorInput.value;
    let showLines = showGridLines.checked;

    function createCells(n){
        const a = new Array(n);
        for(let i=0;i<n;i++){
            a[i] = new Array(n).fill(null);
        }
        return a;
    }

    function resizeCanvas(){
        const size = parseInt(displaySize.value,10) || 640;
        canvas.width = size;
        canvas.height = size;
        cellPixel = size / gridN;
        drawBoard();
    }

    function drawBoard(){
        // background white
        ctx.fillStyle = '#ffffff';
        ctx.fillRect(0,0,canvas.width,canvas.height);

        // draw cells
        for(let r=0;r<gridN;r++){
            for(let c=0;c<gridN;c++){
                const col = cells[r][c];
                if(col){
                    ctx.fillStyle = col;
                    ctx.fillRect(c*cellPixel, r*cellPixel, cellPixel, cellPixel);
                }
            }
        }

        // grid lines
        if(showLines){
            ctx.beginPath();
            ctx.lineWidth = Math.max(1, Math.round(Math.min(1.2, cellPixel*0.03)));
            ctx.strokeStyle = 'rgba(0,0,0,0.08)';
            for(let i=0;i<=gridN;i++){
                const x = Math.round(i*cellPixel) + 0.5;
                ctx.moveTo(x,0);
                ctx.lineTo(x,canvas.height);
            }
            for(let i=0;i<=gridN;i++){
                const y = Math.round(i*cellPixel) + 0.5;
                ctx.moveTo(0,y);
                ctx.lineTo(canvas.width,y);
            }
            ctx.stroke();
        }
    }

    function updatePreview(){
        colorPreview.style.background = currentColor;
        currentInfo.textContent = gridN + ' × ' + gridN;
    }

    // ——— Painting logic ———
    function posToCell(e){
        const rect = canvas.getBoundingClientRect();
        const x = (e.clientX - rect.left) * (canvas.width / rect.width);
        const y = (e.clientY - rect.top) * (canvas.height / rect.height);
        const c = Math.floor(x / cellPixel);
        const r = Math.floor(y / cellPixel);
        if(r<0||r>=gridN||c<0||c>=gridN) return null;
        return {r,c};
    }

    function paintCell(r,c,color){
        cells[r][c] = color;
        ctx.fillStyle = color ? color : '#ffffff';
        ctx.fillRect(c*cellPixel, r*cellPixel, cellPixel, cellPixel);
        if(showLines){
            // redraw small grid stroke around this cell
            ctx.strokeStyle = 'rgba(0,0,0,0.06)';
            ctx.lineWidth = Math.max(1, Math.round(Math.min(1.2, cellPixel*0.03)));
            ctx.strokeRect(c*cellPixel+0.5, r*cellPixel+0.5, cellPixel-1, cellPixel-1);
        }
    }

    canvas.addEventListener('mousedown', (e)=>{
        if(e.button === 2) return; // ignore right for mousedown here
        drawing = true;
        const cell = posToCell(e);
        if(cell){
            const color = isEraser ? null : currentColor;
            paintCell(cell.r,cell.c,color);
        }
    });

    window.addEventListener('mouseup', ()=>{ drawing = false; });

    canvas.addEventListener('mousemove', (e)=>{
        if(!drawing) return;
        const cell = posToCell(e);
        if(cell){
            const color = isEraser ? null : currentColor;
            // avoid unnecessary redraw if same
            if(cells[cell.r][cell.c] !== color) paintCell(cell.r,cell.c,color);
        }
    });

    // right click to erase single cell
    canvas.addEventListener('contextmenu', (e)=>{
        e.preventDefault();
        const cell = posToCell(e);
        if(cell){ paintCell(cell.r,cell.c,null); }
    });

    // keyboard: shift toggles eraser while held
    window.addEventListener('keydown',(e)=>{
        if(e.key === 'Shift'){
            isEraser = true; eraserBtn.classList.add('active');
        }
    });
    window.addEventListener('keyup',(e)=>{
        if(e.key === 'Shift'){
            isEraser = false; eraserBtn.classList.remove('active');
        }
    });

    // UI bindings
    colorInput.addEventListener('input',(e)=>{ currentColor = e.target.value; updatePreview(); });
    eraserBtn.addEventListener('click', ()=>{ isEraser = !isEraser; eraserBtn.classList.toggle('active'); });

    gridSelect.addEventListener('change', ()=>{
        const newN = parseInt(gridSelect.value,10);
        // preserve existing artwork by scaling nearest-neighbor into new grid
        const old = {n:gridN, cells};
        gridN = newN;
        // create new grid and copy
        const newCells = createCells(gridN);
        for(let r=0;r<gridN;r++){
            for(let c=0;c<gridN;c++){
                // map to old cell
                const or = Math.floor(r * old.n / gridN);
                const oc = Math.floor(c * old.n / gridN);
                newCells[r][c] = old.cells[or] ? old.cells[or][oc] : null;
            }
        }
        cells = newCells;
        resizeCanvas();
        updatePreview();
    });

    showGridLines.addEventListener('change', ()=>{ showLines = showGridLines.checked; drawBoard(); });

    displaySize.addEventListener('input', ()=>{ resizeCanvas(); });

    clearBtn.addEventListener('click', ()=>{
        if(!confirm('确定要清空画布吗？')) return;
        cells = createCells(gridN);
        drawBoard();
    });

    fillAllBtn.addEventListener('click', ()=>{
        const col = isEraser ? null : currentColor;
        for(let r=0;r<gridN;r++) for(let c=0;c<gridN;c++) cells[r][c]=col;
        drawBoard();
    });

    // export to PNG: build an offscreen canvas where each cell is CELL_EXPORT px (default 16)
    exportBtn.addEventListener('click', ()=>{
        const CELL_EXPORT = 16; // each pixel in exported image
        const out = document.createElement('canvas');
        out.width = gridN * CELL_EXPORT;
        out.height = gridN * CELL_EXPORT;
        const octx = out.getContext('2d');
        // white background
        octx.fillStyle = '#ffffff';
        octx.fillRect(0,0,out.width,out.height);
        for(let r=0;r<gridN;r++){
            for(let c=0;c<gridN;c++){
                const col = cells[r][c];
                if(col){
                    octx.fillStyle = col;
                    octx.fillRect(c*CELL_EXPORT, r*CELL_EXPORT, CELL_EXPORT, CELL_EXPORT);
                }
            }
        }
        // download
        const url = out.toDataURL('image/png');
        const a = document.createElement('a');
        a.href = url;
        a.download = `pixel-beads-${gridN}x${gridN}.png`;
        document.body.appendChild(a);
        a.click();
        a.remove();
    });

    // export JSON (grid + colors)
    downloadDataBtn.addEventListener('click', ()=>{
        const payload = {n:gridN, cells};
        const blob = new Blob([JSON.stringify(payload)], {type:'application/json'});
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url; a.download = `pixel-beads-${gridN}x${gridN}.json`;
        document.body.appendChild(a); a.click(); a.remove();
        URL.revokeObjectURL(url);
    });

    // initialize
    updatePreview();
    resizeCanvas();
    drawBoard();

    // touch support (mobile)
    function touchToMouse(ev){
        ev.preventDefault();
        const t = ev.changedTouches[0];
        const type = ev.type === 'touchstart' ? 'mousedown' : (ev.type === 'touchend' ? 'mouseup' : 'mousemove');
        const simulated = new MouseEvent(type, {clientX:t.clientX, clientY:t.clientY, bubbles:true});
        canvas.dispatchEvent(simulated);
    }
    canvas.addEventListener('touchstart', touchToMouse, {passive:false});
    canvas.addEventListener('touchmove', touchToMouse, {passive:false});
    canvas.addEventListener('touchend', touchToMouse, {passive:false});

</script>
</body>
</html>
